前面介紹了 CAP 定理,得知了在分散式系統中,資料的同步只能 C3 取 2。
CP 確實是比較難以理解的一個選項,因此在此舉個 CP 的同步方式,
因此,有一個簡單且有效的作法 2PC/3PC 可以幫我們達到以上三個目的。在介紹 2PC/3PC 前,
我們先來思考與試錯,一步一步的了解該項目的重點與解決的項目。
讓兩個獨立的 DB 加入新的資料,並且一起成功或一起失敗,不會有其中一台 DB 成功,一台 DB 失敗。
先回歸操作一台 db 的樣子,
當我們要操作 db 時,我們會先寫一個 server,讓該 server 連上 db 下 CRUD 等操作。
當我們要操作 n 座 db 時,一樣的寫一個 server,讓該 server 連上那些 db 下 CRUD 等操作。
如果以這個案例來定義,我們將會稱該 server 為協調者,而 db 就是我們的參與者。
今天有兩台 db,分別為 A 與 B,A 插入一筆紀錄,B 也必須插入一筆一樣的紀錄。
以下 sql 都是在協調者的操作
time | db A | db B |
---|---|---|
1 | begin | |
2 | insert... | |
3 | commit | |
4 | begin | |
5 | insert... | |
6 | commit |
非常正常的操作,先完成 db A 的操作,再去完成 db B 的操作。
但假設 time5 發生錯誤,db A 就無法 rollback,因為 connection A 的 tx 已經 commit 了,
必須使用手動刪除,兩 db 之間會有軟狀態(暫時性的不一致),與我們想要達成的強一致性不符合。
time | db A | db B |
---|---|---|
1 | begin | |
2 | begin | |
3 | insert... | |
4 | insert... | |
5 | commit | |
6 | commit |
你一句我一句,看似兩者膠著狀態,但其實 time6 發生錯誤,也是如同 try1 一樣無法 rollback。
不管如何,commit 時才發生錯誤都無法達到 rollback。
因為只要其中一個 commit 了,另一個發生錯誤就無法挽回了。
有沒有一個 sql 語句可以模擬 commit,提前知道錯誤,如果有錯誤就 rollback。
如果這個模擬 commit 沒發生錯誤,那麼我們就可以假設,接下來的 commit 基本上不會發生錯誤了。
你可能會想,通常 TX 到 COMMIT 之間,要發生錯誤早就發生了,甚麼錯誤會發生在下 COMMIT 之後呢?
其實最常見的就是違反 UNIQUE constraint,這裡稍微列出一些 COMMIT 之後才會發生的錯誤。
裡面我們撇除物理意外等不可控的因素外,希望至少能製造一個 SQL 語句可以模擬 commit,提前知道錯誤。
希望至少能製造一個 SQL 語句可以模擬 commit,提前知道錯誤。
那麼我們就可以假設,接下來的 commit 基本上不會發生錯誤了。
這個需求,就是 2PC 的重點核心準備階段 prepare statement
。
一般的 tx 就是一個 commit 後直接見真章,是否發生錯誤無法事先得知,這就是我們一般俗稱的一階段提交 1PC。
而這裡我們將加上準備階段 prepare statement
,是否發生錯誤透過準備階段事先得知,
就可以假設,接下來的 commit 基本上不會發生錯誤;如果發生錯誤,那麼就可以在 commit 之前做 rollback。
所以加上PREPARE
這就是我們鼎鼎大名的 2PC(two-phase commit)啦!
而 3PC 也是相同概念,不過分成三個階段canCommit
、preCommit
、doCommit
,
不過由於 3PC 大部分的資料庫不支援,所以就不再此贅述了。
time | db A | db B |
---|---|---|
1 | begin | |
2 | insert... | |
3 | prepare ... | |
4 | begin | |
5 | insert... | |
6 | prepare ... | |
6 | commit | |
7 | commit |
加上了 prepare statement,就可以在 commit 前做個預檢查,確定了不會發生錯誤,在做 commit,大大減少在 commit 發生的錯誤。
不過 2PC 的缺點也是一樣,只能保證 commit 前的錯誤,只是增加一個 statement 來做預檢查。
如網路斷線或斷電,是 commit 後才發生錯誤就真的無法避免了。
上面我們知道了 2PC 的基本原理,再來就是直接上個真實案例,以便學習真正的落地。
請事先準備好 DB,postgres 的max_prepared_transactions
記得打開設定參數,否則會使用 prepare.
在 Postgres 之中,prepare statement 為PREPARE TRANSACTION transaction_id
,詳細介紹可以參考官方介紹。
A 資料庫中的用戶 aa,轉 500 元到 B 資料庫用戶 bb。
需求:
time | db A | db B |
---|---|---|
1 | begin |
|
2 | update account set balance=balance-500 where id='aa; |
|
3 | PREPARE TRANSACTION 'foo'; |
|
4 | begin |
|
5 | update account set balance=balance+500 where id='bb; |
|
6 | PREPARE TRANSACTION 'bar'; |
|
6 | commit PREPARED 'foo'; / rollback PREPARED 'foo'; |
|
7 | commit PREPARED 'bar'; / rollback PREPARED 'bar'; |
今天加上了PREPARE TRANSACTION
,就可以在 commit 時提前發現問題並且 rollback,
便免其中一方事先 commit,造成無法挽回的後果。
我們知道 2PC 的重點其實就是加上prepare statement
,
那麼其實 2PC 的使用範圍就不只是 DB 了,甚至金流 api,不同類型的資料庫串接等等,
只要協調者需要維持強一致性
的需求,那麼 2PC 就是一個很好的選擇。
或許各位讀者早就已經在使用 2PC 了,只是當時不知道那是 2PC 而已,
如今需多 API 等概念都使用到了 2PC。
2PC 也可以當作共識演算法,因為要讓每個節點都能達到相同的值。
其實 2PC 也是 Paxos 算法的一種簡化版本。